package educrypt.ciphers.playfair;

import java.util.ArrayList;
import java.util.List;

import educrypt.commons.Alphabet;
import educrypt.commons.Text;
import educrypt.commons.util.TextUtil;
import educrypt.exceptions.EduCryptException;
import educrypt.i18n.Messages;
import educrypt.log.ObservableLogger;
import educrypt.log.SimpleLogger;

public class Playfair {

	private static int NGRAM_SIZE = 2;
	private static int ALPHABET_LENGTH = 25;

	public Alphabet alphabet;
	public PlayfairKeyTable key;
	public char separatorSymbol;

	/* STATIC METHODS */
	public static String formatKeyword(String keyword, Alphabet alphabet) {
		keyword = keyword.toLowerCase();
		// Remove unknown symbols
		keyword = alphabet.filterKnownSymbols(keyword);
		// Remove repeated symbols
		keyword = TextUtil.removeRepeatedSymbols(keyword);
		// Length must be under 25 (alphabet length)
		if (keyword.length() > ALPHABET_LENGTH) {
			keyword = keyword.substring(0, ALPHABET_LENGTH);
		}
		return keyword;
	}

	public static String formatAlphabetSymbols(String symbols)
			throws EduCryptException {
		String formattedAlphabetSymbols = "";
		if (symbols.length() < ALPHABET_LENGTH) {
			throw new EduCryptException(
					Messages.getString("errPlayfairAlphabet"));
		} else {
			formattedAlphabetSymbols = symbols.substring(0, ALPHABET_LENGTH);
		}
		return formattedAlphabetSymbols;
	}

	/* END STATIC METHODS */

	public Playfair(Alphabet alphabet, String keyword) {
		// checkKeyword();
		this.alphabet = alphabet;
		this.key = new PlayfairKeyTable(keyword, alphabet);
		// The last symbol will be the separator
		this.separatorSymbol = this.alphabet
				.getSymbolAt(this.alphabet.size() - 1);

		this.logCipherDetails(keyword, separatorSymbol);

	}

	// Workaround to avoid messages in the evaluation of criptanalysis
	public Playfair(Alphabet alphabet, String keyword, boolean b) {
		// checkKeyword();
		this.alphabet = alphabet;
		this.key = new PlayfairKeyTable(keyword, alphabet);
		// The last symbol will be the separator
		this.separatorSymbol = this.alphabet
				.getSymbolAt(this.alphabet.size() - 1);

	}

	private void logCipherDetails(String keyword, char separatorSymbol) {
		// Log to console
		SimpleLogger logger = ObservableLogger.getInstance();
		logger.log(SimpleLogger.TITLE,
				"---- " + Messages.getString("cipherPlayfair") + " ----");

		logger.log(SimpleLogger.TITLE, Messages.getString("guiAlphabet"));
		logger.log(SimpleLogger.CONTENT, alphabet.getSymbols());

		logger.log(SimpleLogger.TITLE,
				Messages.getString("guiRepeatedSymbolSeparator"));
		logger.log(SimpleLogger.CONTENT, Character.toString(separatorSymbol));

		logger.log(SimpleLogger.TITLE, Messages.getString("guiKeyword"));
		logger.log(SimpleLogger.CONTENT, keyword);

		logger.log(SimpleLogger.TITLE,
				Messages.getString("guiGeneratedKeyTable"));
		logger.log(SimpleLogger.CONTENT, key.toString());
	}

	public Text encode(Text plainText) {
		long start = System.currentTimeMillis();
		PlayfairBigrams bigrams = new PlayfairBigrams(plainText.getContent(),
				this.alphabet, this.separatorSymbol);
		StringBuffer cipherText = new StringBuffer(plainText.size());
		for (int i = 0; i < bigrams.size(); i++) {
			String bigram = bigrams.getBigram(i);
			cipherText.append(encodeBigram(bigram));
		}
		System.out.println(System.currentTimeMillis() - start);
		return new Text(cipherText.toString());
	}

	private StringBuffer encodeBigram(String bigram) {

		// Known symbols position
		List<Integer> validSymbolsPosition = this
				.getValidSymbolsPosition(bigram);
		// Bigram with known symbols
		StringBuffer filteredBigram = filterBigram(bigram, validSymbolsPosition);

		boolean isFirstSymbolUpperCase = Character.isUpperCase(filteredBigram
				.charAt(0));
		boolean isSecondSymbolUpperCase = Character.isUpperCase(filteredBigram
				.charAt(1));

		// Position of bigram symbols in key table
		TablePosition[] bigramPositionOnKeyTable = key
				.getSymbolsPosition(filteredBigram.toString());

		// Application of encoding rules
		TablePosition[] cipherBigramPositionOnKeyTable = this
				.getCipherBigramPositionOnKeyTable(bigramPositionOnKeyTable);

		// Cipher symbols
		char cipherFirstSymbol = key
				.getSymbolAt(cipherBigramPositionOnKeyTable[0]);
		if (isFirstSymbolUpperCase) {
			cipherFirstSymbol = Character.toUpperCase(cipherFirstSymbol);
		}
		char cipherSecondSymbol = key
				.getSymbolAt(cipherBigramPositionOnKeyTable[1]);
		if (isSecondSymbolUpperCase) {
			cipherSecondSymbol = Character.toUpperCase(cipherSecondSymbol);
		}

		StringBuffer cipherBigram = new StringBuffer();

		// Bigram substitution of cipher symbols
		for (char symbol : bigram.toCharArray()) {
			if (symbol == filteredBigram.charAt(0)) {
				cipherBigram.append(cipherFirstSymbol);
			} else if (symbol == filteredBigram.charAt(1)) {
				cipherBigram.append(cipherSecondSymbol);
			} else {
				cipherBigram.append(symbol);
			}
		}
		return cipherBigram;
	}

	private List<Integer> getValidSymbolsPosition(String bigram) {
		List<Integer> bigramPosition = new ArrayList<Integer>(NGRAM_SIZE);
		for (int i = 0; i < bigram.length(); i++) {
			char symbol = bigram.charAt(i);
			boolean isValidSymbol = alphabet.containsSymbol(symbol);
			if (isValidSymbol) {
				bigramPosition.add(i);
			}
		}
		assert bigramPosition.size() == NGRAM_SIZE;
		return bigramPosition;

	}

	private StringBuffer filterBigram(String bigram,
			List<Integer> validBigramPosition) {
		StringBuffer filteredBigram = new StringBuffer();
		for (int position : validBigramPosition) {
			filteredBigram.append(bigram.charAt(position));
		}
		return filteredBigram;
	}

	private TablePosition[] getCipherBigramPositionOnKeyTable(
			TablePosition[] bigramPositionOnKeyTable) {
		TablePosition firstSymbolPosition = bigramPositionOnKeyTable[0];
		TablePosition secondSymbolPosition = bigramPositionOnKeyTable[1];

		TablePosition newFirstSymbolPosition = new TablePosition();
		TablePosition newSecondSymbolPosition = new TablePosition();

		// Symbols shouldn't be in the same position, but it can happen with the
		// separator symbol
		if (firstSymbolPosition.equals(secondSymbolPosition)
				&& this.key.getSymbolAt(firstSymbolPosition) == separatorSymbol) {
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newFirstSymbolPosition.setColumn(firstSymbolPosition.getColumn());

			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			newSecondSymbolPosition.setColumn(secondSymbolPosition.getColumn());

		} else if (firstSymbolPosition.getColumn() == secondSymbolPosition
				.getColumn()) {
			// row -> row + 1 (wrap)
			newFirstSymbolPosition.setRow(key
					.getNextPosition(firstSymbolPosition.getRow()));
			newSecondSymbolPosition.setRow(key
					.getNextPosition(secondSymbolPosition.getRow()));
			// same column
			newFirstSymbolPosition.setColumn(firstSymbolPosition.getColumn());

			newSecondSymbolPosition.setColumn(secondSymbolPosition.getColumn());

		} else if (firstSymbolPosition.getRow() == secondSymbolPosition
				.getRow()) {

			// same row
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			// column -> column + 1 (wrap)
			newFirstSymbolPosition.setColumn(key
					.getNextPosition(firstSymbolPosition.getColumn()));
			newSecondSymbolPosition.setColumn(key
					.getNextPosition(secondSymbolPosition.getColumn()));

		} else {
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newFirstSymbolPosition.setColumn(secondSymbolPosition.getColumn());

			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			newSecondSymbolPosition.setColumn(firstSymbolPosition.getColumn());
		}

		TablePosition[] cipherBigramPositionOnKeyTable = new TablePosition[NGRAM_SIZE];
		cipherBigramPositionOnKeyTable[0] = newFirstSymbolPosition;
		cipherBigramPositionOnKeyTable[1] = newSecondSymbolPosition;

		return cipherBigramPositionOnKeyTable;
	}

	public Text decode(Text cipherText) {
		PlayfairBigrams bigrams = new PlayfairBigrams(cipherText.getContent(),
				this.alphabet, separatorSymbol);
		StringBuffer plainText = new StringBuffer(cipherText.size());
		for (int i = 0; i < bigrams.size(); i++) {
			String bigram = bigrams.getBigram(i);
			plainText.append(decodeBigram(bigram));
		}
		return new Text(plainText.toString());
	}

	public StringBuffer decodeBigram(String bigram) {
		// Known symbols position
		List<Integer> validSymbolsPosition = this
				.getValidSymbolsPosition(bigram);
		// Bigram with known symbols
		StringBuffer filteredBigram = filterBigram(bigram, validSymbolsPosition);
		boolean isFirstSymbolUpperCase = Character.isUpperCase(filteredBigram
				.charAt(0));
		boolean isSecondSymbolUpperCase = Character.isUpperCase(filteredBigram
				.charAt(1));

		// Position of bigram symbols in key table
		TablePosition[] bigramPositionOnKeyTable = key
				.getSymbolsPosition(filteredBigram.toString());

		// Application of encoding rules
		TablePosition[] plainBigramPositionOnKeyTable = this
				.getPlainBigramPositionOnKeyTable(bigramPositionOnKeyTable);

		// Cipher symbols
		char cipherFirstSymbol = key
				.getSymbolAt(plainBigramPositionOnKeyTable[0]);
		if (isFirstSymbolUpperCase) {
			cipherFirstSymbol = Character.toUpperCase(cipherFirstSymbol);
		}
		char cipherSecondSymbol = key
				.getSymbolAt(plainBigramPositionOnKeyTable[1]);
		if (isSecondSymbolUpperCase) {
			cipherSecondSymbol = Character.toUpperCase(cipherSecondSymbol);
		}

		StringBuffer plainBigram = new StringBuffer();

		// Bigram substitution of cipher symbols
		for (char symbol : bigram.toCharArray()) {
			if (symbol == filteredBigram.charAt(0)) {
				plainBigram.append(cipherFirstSymbol);
			} else if (symbol == filteredBigram.charAt(1)) {
				plainBigram.append(cipherSecondSymbol);
			} else {
				plainBigram.append(symbol);
			}
		}
		return plainBigram;
	}

	private TablePosition[] getPlainBigramPositionOnKeyTable(
			TablePosition[] bigramPositionOnKeyTable) {
		TablePosition firstSymbolPosition = bigramPositionOnKeyTable[0];
		TablePosition secondSymbolPosition = bigramPositionOnKeyTable[1];

		TablePosition newFirstSymbolPosition = new TablePosition();
		TablePosition newSecondSymbolPosition = new TablePosition();

		// Symbols shouldn't be in the same position, but it can happen with the
		// separator symbol
		if (firstSymbolPosition.equals(secondSymbolPosition)
				&& this.key.getSymbolAt(firstSymbolPosition) == separatorSymbol) {
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newFirstSymbolPosition.setColumn(firstSymbolPosition.getColumn());

			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			newSecondSymbolPosition.setColumn(secondSymbolPosition.getColumn());

		} else if (firstSymbolPosition.getColumn() == secondSymbolPosition
				.getColumn()) {
			// row -> row + 1 (wrap)
			newFirstSymbolPosition.setRow(key
					.getPreviousPosition(firstSymbolPosition.getRow()));
			newSecondSymbolPosition.setRow(key
					.getPreviousPosition(secondSymbolPosition.getRow()));
			// same column
			newFirstSymbolPosition.setColumn(firstSymbolPosition.getColumn());

			newSecondSymbolPosition.setColumn(secondSymbolPosition.getColumn());

		} else if (firstSymbolPosition.getRow() == secondSymbolPosition
				.getRow()) {

			// same row
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			// column -> column + 1 (wrap)
			newFirstSymbolPosition.setColumn(key
					.getPreviousPosition(firstSymbolPosition.getColumn()));
			newSecondSymbolPosition.setColumn(key
					.getPreviousPosition(secondSymbolPosition.getColumn()));

		} else {
			newFirstSymbolPosition.setRow(firstSymbolPosition.getRow());
			newFirstSymbolPosition.setColumn(secondSymbolPosition.getColumn());

			newSecondSymbolPosition.setRow(secondSymbolPosition.getRow());
			newSecondSymbolPosition.setColumn(firstSymbolPosition.getColumn());
		}

		TablePosition[] plainBigramPositionOnKeyTable = new TablePosition[NGRAM_SIZE];
		plainBigramPositionOnKeyTable[0] = newFirstSymbolPosition;
		plainBigramPositionOnKeyTable[1] = newSecondSymbolPosition;

		return plainBigramPositionOnKeyTable;
	}

}
